using System;
using System.Collections;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using UnityEngine.Events;

namespace DanMuHelper.Tools
{
    /// <summary>
    ///强制设置Unity游戏窗口的长宽比。你可以调整窗口的大小，他会强制保持一定比例
    ///通过拦截窗口大小调整事件(WindowProc回调)并相应地修改它们来实现的
    ///也可以用像素为窗口设置最小/最大宽度和高度
    ///长宽比和最小/最大分辨率都与窗口区域有关，标题栏和边框不包括在内
    ///该脚本还将在应用程序处于全屏状态时强制设置长宽比。当你切换到全屏，
    ///应用程序将自动设置为当前显示器上可能的最大分辨率，而仍然保持固定比。如果显示器没有相同的宽高比，则会在左/右或上/下添加黑条
    ///确保你在PlayerSetting中设置了“Resizable Window”，否则无法调整大小
    ///如果取消不支持的长宽比在PlayerSetting中设置“Supported Aspect Rations”
    ///注意:因为使用了WinAPI，所以只能在Windows上工作。在Windows 10上测试过
    /// </summary>
    [DisallowMultipleComponent]
    public class AspectRatioController : MonoBehaviour
    {
        // /// <summary>
        // /// 每当窗口分辨率改变或用户切换全屏时，都会触发此事件
        // ///  参数是新的宽度、高度和全屏状态(true表示全屏)
        // /// </summary>
        public UnityEvent resolutionChangedEvent = new UnityEvent();
        static int tempCount = 1;
        static int lastTempCount = 1;

        // 如果为false，则阻止切换到全屏
        [SerializeField]
        private bool allowFullscreen = false;

        // 长宽比的宽度和高度
        [SerializeField]
        private float aspectRatioWidth = 9;

        [SerializeField]
        private float aspectRatioHeight = 16;

        // 最小值和最大值的窗口宽度/高度像素
        [SerializeField]
        private int minWidthPixel = (int)(900.0f * (9.0f / 16.0f));

        [SerializeField]
        private int minHeightPixel = 900;

        [SerializeField]
        private int maxWidthPixel = (int)(4000.0f * (9.0f / 16.0f));

        [SerializeField]
        private int maxHeightPixel = 4000;

        private static int MinWidthPixel = 512;
        private static int MinHeightPixel = 512;
        private static int MaxWidthPixel = 2048;
        private static int MaxHeightPixel = 2048;

        // 当前锁定长宽比。
        private static float aspect;

        // 窗口的宽度和高度。不包括边框和窗口标题栏
        // 当调整窗口大小时，就会设置这些值
        private static int setWidth = -1;
        private static int setHeight = -1;

        // 最后一帧全屏状态。
        private bool wasFullscreenLastFrame;

        // 是否初始化了AspectRatioController
        // 一旦注册了WindowProc回调函数，就将其设置为true
        private bool started;

        // 显示器的宽度和高度。这是窗口当前打开的监视器
        private int pixelHeightOfCurrentScreen;
        private int pixelWidthOfCurrentScreen;

        //一旦用户请求终止applaction，则将其设置为true
        private bool quitStarted;

        // WinAPI相关定义
        #region WINAPI

        // 当窗口调整时,WM_SIZING消息通过WindowProc回调发送到窗口
        private const int WM_SIZING = 0x214;

        // WM大小调整消息的参数
        private const int WMSZ_LEFT = 1;
        private const int WMSZ_RIGHT = 2;
        private const int WMSZ_TOP = 3;
        private const int WMSZ_BOTTOM = 6;

        // 获取指向WindowProc函数的指针
        private const int GWLP_WNDPROC = -4;

        // 委托设置为新的WindowProc回调函数
        private delegate IntPtr WndProcDelegate(
            IntPtr hWnd,
            uint msg,
            IntPtr wParam,
            IntPtr lParam
        );

        //静态委托实例
        private static WndProcDelegate wndProcDelegate = wndProc;

        //如果用户不会乱点的话，也可以用这个方法取代EnumThreadWindows
        [DllImport("user32.dll")]
        private static extern IntPtr GetActiveWindow();

        // 检索调用线程的线程标识符
        [DllImport("kernel32.dll")]
        private static extern uint GetCurrentThreadId();

        // 检索指定窗口所属类的名称
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

        // 通过将句柄传递给每个窗口，依次传递给应用程序定义的回调函数，枚举与线程关联的所有非子窗口
        [DllImport("user32.dll")]
        private static extern bool EnumThreadWindows(
            uint dwThreadId,
            EnumWindowsProc lpEnumFunc,
            IntPtr lParam
        );

        private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

        //静态委托实例
        private static EnumWindowsProc enumProcDelegate = enumProc;

        // 将消息信息传递给指定的窗口过程
        [DllImport("user32.dll")]
        private static extern IntPtr CallWindowProc(
            IntPtr lpPrevWndFunc,
            IntPtr hWnd,
            uint Msg,
            IntPtr wParam,
            IntPtr lParam
        );

        // 检索指定窗口的边框的尺寸
        // 尺寸是在屏幕坐标中给出的，它是相对于屏幕左上角的
        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool GetWindowRect(IntPtr hwnd, ref RECT lpRect);

        //检索窗口客户区域的坐标。客户端坐标指定左上角
        //以及客户区的右下角。因为客户机坐标是相对于左上角的
        //在窗口的客户区域的角落，左上角的坐标是(0,0)
        [DllImport("user32.dll")]
        private static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);

        // 更改指定窗口的属性。该函数还将指定偏移量的32位(长)值设置到额外的窗口内存中
        [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)]
        private static extern IntPtr SetWindowLong32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

        //更改指定窗口的属性。该函数还在额外的窗口内存中指定的偏移量处设置一个值
        [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto)]
        private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

        //用于查找窗口句柄的Unity窗口类的名称
        private const string UNITY_WND_CLASSNAME = "UnityWndClass";

        // Unity窗口的窗口句柄
        private static IntPtr unityHWnd;

        // 指向旧WindowProc回调函数的指针
        private static IntPtr oldWndProcPtr;

        // 指向我们自己的窗口回调函数的指针
        private IntPtr newWndProcPtr;

        /// <summary>
        /// WinAPI矩形定义。
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }

        #endregion

        void Start()
        {
            //设为最小最大高宽度设为静态变量
            setMinAndMaxPixel();

            // 不要在Unity编辑器中注册WindowProc回调函数，它会指向Unity编辑器窗口，而不是Game视图

#if UNITY_STANDALONE_WIN&&!UNITY_EDITOR
            //注册回调，然后应用程序想要退出
            Application.wantsToQuit += ApplicationWantsToQuit;

            // 找到主Unity窗口的窗口句柄
            EnumThreadWindows(GetCurrentThreadId(), enumProcDelegate, IntPtr.Zero);

            // 将长宽比应用于当前分辨率
            SetAspectRatio(aspectRatioWidth, aspectRatioHeight, true);

            // 保存当前的全屏状态
            wasFullscreenLastFrame = Screen.fullScreen;

            // Register (replace) WindowProc callback。每当一个窗口事件被触发时，这个函数都会被调用
            //例如调整大小或移动窗口
            //保存旧的WindowProc回调函数，因为必须从新回调函数中调用它
            newWndProcPtr = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
            oldWndProcPtr = SetWindowLong(unityHWnd, GWLP_WNDPROC, newWndProcPtr);

            // 初始化完成
            started = true;
            resolutionChangedEvent?.Invoke();
#else
            // Disable for good
            this.enabled = false;
#endif
        }

        //由于变成静态方法，所以在最开始静态变量加载非静态变量的值
        void setMinAndMaxPixel()
        {
            MinWidthPixel = minWidthPixel;
            MaxWidthPixel = maxWidthPixel;
            MinHeightPixel = minHeightPixel;
            MaxHeightPixel = maxHeightPixel;
        }

        private static void resolutionChangedFunc()
        {
            tempCount++;
        }

        [AOT.MonoPInvokeCallback(typeof(EnumWindowsProc))]
        private static bool enumProc(IntPtr hWnd, IntPtr lParam)
        {
            var classText = new StringBuilder(UNITY_WND_CLASSNAME.Length + 1);
            GetClassName(hWnd, classText, classText.Capacity);

            if (classText.ToString() == UNITY_WND_CLASSNAME)
            {
                unityHWnd = hWnd;
                return false;
            }
            return true;
        }

        /// <summary>
        ///将目标长宽比设置为给定的长宽比。
        /// </summary>
        /// <param name="newAspectWidth">宽高比的新宽度</param>
        /// <param name="newAspectHeight">纵横比的新高度</param>
        /// <param name="apply">true，当前窗口分辨率将立即调整以匹配新的纵横比 false，则只在下次手动调整窗口大小时执行此操作</param>
        public void SetAspectRatio(float newAspectWidth, float newAspectHeight, bool apply)
        {
            //计算新的纵横比
            aspectRatioWidth = newAspectWidth;
            aspectRatioHeight = newAspectHeight;
            aspect = aspectRatioWidth / aspectRatioHeight;

            // 调整分辨率以匹配长宽比(触发WindowProc回调)
            if (apply)
            {
                Screen.SetResolution(
                    Screen.width,
                    Mathf.RoundToInt(Screen.width / aspect),
                    Screen.fullScreen
                );
            }
        }

        /// <summary>
        /// WindowProc回调。应用程序定义的函数，用来处理发送到窗口的消息
        /// </summary>
        /// <param name="msg">用于标识事件的消息</param>
        /// <param name="wParam">额外的信息信息。该参数的内容取决于uMsg参数的值 </param>
        /// <param name="lParam">其他消息的信息。该参数的内容取决于uMsg参数的值 </param>
        /// <returns></returns>
        [AOT.MonoPInvokeCallback(typeof(WndProcDelegate))]
        private static IntPtr wndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
        {
            // 检查消息类型
            // resize事件
            if (msg == WM_SIZING)
            {
                // 获取窗口大小结构体
                RECT rc = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT));

                // 计算窗口边框的宽度和高度
                RECT windowRect = new RECT();
                GetWindowRect(unityHWnd, ref windowRect);

                RECT clientRect = new RECT();
                GetClientRect(unityHWnd, ref clientRect);

                int borderWidth =
                    windowRect.Right - windowRect.Left - (clientRect.Right - clientRect.Left);
                int borderHeight =
                    windowRect.Bottom - windowRect.Top - (clientRect.Bottom - clientRect.Top);

                // 在应用宽高比之前删除边框(包括窗口标题栏)
                rc.Right -= borderWidth;
                rc.Bottom -= borderHeight;

                // 限制窗口大小
                int newWidth = Mathf.Clamp(rc.Right - rc.Left, MinWidthPixel, MaxWidthPixel);
                int newHeight = Mathf.Clamp(rc.Bottom - rc.Top, MinHeightPixel, MaxHeightPixel);

                // 根据纵横比和方向调整大小
                switch (wParam.ToInt32())
                {
                    case WMSZ_LEFT:
                        rc.Left = rc.Right - newWidth;
                        rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
                        break;
                    case WMSZ_RIGHT:
                        rc.Right = rc.Left + newWidth;
                        rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
                        break;
                    case WMSZ_TOP:
                        rc.Top = rc.Bottom - newHeight;
                        rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
                        break;
                    case WMSZ_BOTTOM:
                        rc.Bottom = rc.Top + newHeight;
                        rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
                        break;
                    case WMSZ_RIGHT + WMSZ_BOTTOM:
                        rc.Right = rc.Left + newWidth;
                        rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
                        break;
                    case WMSZ_RIGHT + WMSZ_TOP:
                        rc.Right = rc.Left + newWidth;
                        rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
                        break;
                    case WMSZ_LEFT + WMSZ_BOTTOM:
                        rc.Left = rc.Right - newWidth;
                        rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
                        break;
                    case WMSZ_LEFT + WMSZ_TOP:
                        rc.Left = rc.Right - newWidth;
                        rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
                        break;
                }

                // 保存实际分辨率,不包括边界
                setWidth = rc.Right - rc.Left;
                setHeight = rc.Bottom - rc.Top;

                // 添加边界
                rc.Right += borderWidth;
                rc.Bottom += borderHeight;

                // 触发分辨率更改事件
                resolutionChangedFunc();

                // 回写更改的窗口参数
                Marshal.StructureToPtr(rc, lParam, true);
            }

            // 调用原始的WindowProc函数
            return CallWindowProc(oldWndProcPtr, hWnd, msg, wParam, lParam);
        }

        void Update()
        {
            // 如果不允许全屏，则阻止切换到全屏
            if (!allowFullscreen && Screen.fullScreen)
            {
                Screen.fullScreen = false;
            }

            if (Screen.fullScreen && !wasFullscreenLastFrame)
            {
                //切换到全屏检测,设置为最大屏幕分辨率，同时保持长宽比
                int height;
                int width;

                //根据当前长宽比和显示器的比例进行比较，上下或左右添加黑边
                bool blackBarsLeftRight =
                    aspect < (float)pixelWidthOfCurrentScreen / pixelHeightOfCurrentScreen;

                if (blackBarsLeftRight)
                {
                    height = pixelHeightOfCurrentScreen;
                    width = Mathf.RoundToInt(pixelHeightOfCurrentScreen * aspect);
                }
                else
                {
                    width = pixelWidthOfCurrentScreen;
                    height = Mathf.RoundToInt(pixelWidthOfCurrentScreen / aspect);
                }

                Screen.SetResolution(width, height, true);
                resolutionChangedFunc();
            }
            else if (!Screen.fullScreen && wasFullscreenLastFrame)
            {
                // 从全屏切换到检测到的窗口。设置上一个窗口的分辨率。
                Screen.SetResolution(setWidth, setHeight, false);
                resolutionChangedFunc();
            }
            else if (
                !Screen.fullScreen
                && setWidth != -1
                && setHeight != -1
                && (Screen.width != setWidth || Screen.height != setHeight)
            )
            {
                //根据高度设置宽度，因为Aero Snap不会触发WM_SIZING。
                setHeight = Screen.height;
                setWidth = Mathf.RoundToInt(Screen.height * aspect);

                Screen.SetResolution(setWidth, setHeight, Screen.fullScreen);
                resolutionChangedFunc();
            }
            else if (!Screen.fullScreen)
            {
                // 保存当前屏幕的分辨率
                // 下次切换到全屏时，此分辨率将被设置为窗口分辨率
                // 只有高度，如果需要，宽度将根据高度和长宽比设置，以确保长宽比保持在全屏模式
                pixelHeightOfCurrentScreen = Screen.currentResolution.height;
                pixelWidthOfCurrentScreen = Screen.currentResolution.width;
            }

            //保存下一帧的全屏状态
            wasFullscreenLastFrame = Screen.fullScreen;

            // 当游戏窗口调整大小时，在编辑器中触发分辨率改变事件。
#if UNITY_EDITOR
            if (Screen.width != setWidth || Screen.height != setHeight)
            {
                setWidth = Screen.width;
                setHeight = Screen.height;
                resolutionChangedFunc();
            }
#endif
            if (lastTempCount != tempCount)
            {
                resolutionChangedEvent.Invoke();
                lastTempCount = tempCount;
            }
        }

        /// <summary>
        /// 调用SetWindowLong32或SetWindowLongPtr64，取决于可执行文件是32位还是64位。
        /// 这样，我们就可以同时构建32位和64位的可执行文件而不会遇到问题。
        /// </summary>
        /// <param name="hWnd">The window handle.</param>
        /// <param name="nIndex">要设置的值的从零开始的偏移量</param>
        /// <param name="dwNewLong">The replacement value.</param>
        /// <returns>返回值是指定偏移量的前一个值。否则零.</returns>
        private static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
        {
            //32位系统
            if (IntPtr.Size == 4)
            {
                return SetWindowLong32(hWnd, nIndex, dwNewLong);
            }
            return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
        }

        /// <summary>
        /// 退出时调用。 返回false将中止并使应用程序保持活动。True会让它退出。
        /// </summary>
        /// <returns></returns>
        private bool ApplicationWantsToQuit()
        {
            //仅允许在应用程序初始化后退出。
            if (!started)
                return false;

            //延迟退出，clear up
            if (!quitStarted)
            {
                StartCoroutine("DelayedQuit");
                return false;
            }
            return true;
        }

        /// <summary>
        /// 恢复旧的WindowProc回调，然后退出。
        /// </summary>
        IEnumerator DelayedQuit()
        {
            // 重新设置旧的WindowProc回调,如果检测到WM_CLOSE,这将在新的回调本身中完成, 64位没问题，32位可能会造成闪退

            SetWindowLong(unityHWnd, GWLP_WNDPROC, oldWndProcPtr);

            yield return new WaitForEndOfFrame();

            quitStarted = true;
            Application.Quit();
        }
    }
}
